package cc.alcina.framework.gwt.client.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import cc.alcina.framework.common.client.util.CommonConstants;
import cc.alcina.framework.common.client.util.CommonUtils;
import cc.alcina.framework.gwt.client.ClientLayerLocator;
import cc.alcina.framework.gwt.client.ClientNotifications;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Text;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.RootPanel;

public class DomUtils implements NodeFromXpathProvider {
	private static final String DOM_XPATH_MAP = "dom-xpath-map";

	private static final String TEXT_MARKER = "TEXT()";

	private boolean useXpathMap = true;

	private Node lastContainer = null;

	private Map<String, Node> xpathMap;

	private boolean debug = false;

	private static final String HTML_INVISIBLE_CONTENT_ELEMENTS = ",STYLE,TEXTAREA,SCRIPT,INPUT,SELECT,";

	public static boolean isBlockHTMLElement(Element e) {
		return CommonConstants.HTML_BLOCKS.contains(","
				+ e.getTagName().toUpperCase() + ",");
	}

	private NodeFromXpathProvider nodeProvider = null;

	private ClientNodeIterator walker;

	public DomUtils() {
		invalidateUnwrapOrIgnoreCache();
	}

	private static void addVisibleTextNodes(Element element, List<Text> texts) {
		NodeList<Node> nl = element.getChildNodes();
		int length = nl.getLength();
		for (int i = 0; i < length; i++) {
			Node node = nl.getItem(i);
			if (node.getNodeType() == Node.TEXT_NODE) {
				texts.add((Text) node);
			} else if (node.getNodeType() == Node.ELEMENT_NODE
					&& !isInvisibleContentElement((Element) node)) {
				addVisibleTextNodes((Element) node, texts);
			}
		}
	}

	public static List<Text> getVisibleTextNodes(Element root) {
		List<Text> texts = new ArrayList<Text>();
		addVisibleTextNodes(root, texts);
		return texts;
	}

	public static boolean isInvisibleContentElement(Element elt) {
		return HTML_INVISIBLE_CONTENT_ELEMENTS.contains(","
				+ elt.getTagName().toUpperCase() + ",");
	}

	public static Text getFirstNonWhitespaceTextChild(Node node) {
		NodeList nl = node.getChildNodes();
		for (int i = 0; i < nl.getLength(); i++) {
			node = nl.getItem(i);
			if (node.getNodeType() == Node.ELEMENT_NODE) {
				Text t = getFirstNonWhitespaceTextChild(node);
				if (t != null) {
					return t;
				}
			}
			if (node.getNodeType() == Node.TEXT_NODE) {
				if (!TextUtils.isWhitespaceOrEmpty(node.getNodeValue())) {
					return (Text) node;
				}
			}
		}
		return null;
	}

	public static boolean isVisibleAncestorChain(Element e) {
		while (e != null) {
			if (e.getStyle().getDisplay().equals(Display.NONE.getCssName())) {
				return false;
			}
			e = e.getParentElement();
		}
		return true;
	}

	public Node findXpathWithIndexedText(String xpathStr, Node container) {
		if (nodeProvider != null) {
			return nodeProvider.findXpathWithIndexedText(xpathStr, container);
		}
		if (xpathStr.length() == 0) {
			return container;
		}
		String ucXpath = xpathStr.toUpperCase();
		if (useXpathMap) {
			if (lastContainer != container) {
				lastContainer = container;
				walker = new ClientNodeIterator(container,
						ClientNodeIterator.SHOW_ELEMENT
								| ClientNodeIterator.SHOW_TEXT);
				xpathMap = new LinkedHashMap<String, Node>();
				ClientNotifications notifications = ClientLayerLocator.get()
						.notifications();
				if (notifications != null) {
					notifications.metricLogStart(DOM_XPATH_MAP);
				}
				generateMap((Element) container, "", xpathMap);
				if (notifications != null) {
					notifications.metricLogEnd(DOM_XPATH_MAP);
				}
			}
			Node node = xpathMap.get(ucXpath);
			String singleTextPoss = "TEXT()[1]";
			String possiblyWrappedTextPost = "TEXT()";
			if (node == null && ucXpath.endsWith(singleTextPoss)) {
				node = xpathMap.get(ucXpath.substring(0, ucXpath.length() - 3));
			}
			if (node == null && ucXpath.endsWith(possiblyWrappedTextPost)) {
				node = xpathMap.get(ucXpath + "[1]");
			}
			return node;
		} else {
			if (lastContainer != container) {
				lastContainer = container;
				walker = new ClientNodeIterator(container,
						ClientNodeIterator.SHOW_ELEMENT
								| ClientNodeIterator.SHOW_TEXT);
			}
		}
		// keep in sync with sedomutils
		Node current = container;
		String[] sections = ucXpath.split("/");
		for (String section : sections) {
			int index = 1;
			String tagName = section;
			if (section.contains("[")) {
				index = Integer.valueOf(section.substring(
						section.indexOf("[") + 1, section.length() - 1));
				tagName = section.substring(0, section.indexOf("["))
						.toUpperCase();
			}
			boolean asText = tagName.equals(DomUtils.TEXT_MARKER);
			NodeList<Node> nodes = current.getChildNodes();
			current = null;
			Node last = null;
			int length = nodes.getLength();
			for (int i = 0; i < length; i++) {
				Node node = nodes.getItem(i);
				node = unwrapOrIgnore(node);
				if (node == null) {
					continue;
				}
				boolean foundIndexed = false;
				if (asText) {
					foundIndexed = node.getNodeType() == Node.TEXT_NODE;
					if (foundIndexed && last != null
							&& last.getNodeType() == Node.TEXT_NODE) {
						continue;// ignore, split
					}
				} else {
					foundIndexed = node.getNodeType() == Node.ELEMENT_NODE
							&& node.getNodeName().equals(tagName);
				}
				if (foundIndexed) {
					index--;
					if (index == 0) {
						current = node;
						break;
					}
				}
				last = node;
			}
			if (current == null) {
				return null;
			}
		}
		return current;
	}

	public boolean isUseXpathMap() {
		return this.useXpathMap;
	}

	public static String ignoreableElementIdPrefix = "IGNORE__";

	private Map<Node, Node> unwrapOrIgnoreCache;

	private Node unwrapOrIgnore(Node node) {
		short nodeType = node.getNodeType();
		if (nodeType != Node.ELEMENT_NODE) {
			return node;
		}
		if (unwrapOrIgnoreCache.containsKey(node)) {
			return unwrapOrIgnoreCache.get(node);
		}
		Node result = node;
		Element e = (Element) node;
		if (e.getAttribute("id").startsWith(ignoreableElementIdPrefix)) {
			if (node.getNodeName().equalsIgnoreCase("span")) {
				walker.setCurrentNode(node);
				while (walker.nextNode() != null) {
					node = walker.getCurrentNode();
					if (node.getNodeType() == Node.ELEMENT_NODE
							&& ((Element) node).getAttribute("id").startsWith(
									ignoreableElementIdPrefix)) {
					} else {
						result = node;
						break;
					}
				}
			} else {
				result = null;
			}
		}
		unwrapOrIgnoreCache.put(node, result);
		return result;
	}

	public void setUseXpathMap(boolean useXpathMap) {
		this.useXpathMap = useXpathMap;
	}

	public void generateMap(Element elt, String prefix,
			Map<String, Node> xpathMap) {
		generateMap0(elt, prefix, xpathMap);
	}

	private void generateMap0(Element elt, String prefix,
			Map<String, Node> xpathMap) {
		if (elt == null) {
			return;
		}
		Map<String, Integer> total = new HashMap<String, Integer>();
		Map<String, Integer> current = new HashMap<String, Integer>();
		NodeList<Node> nodes = elt.getChildNodes();
		if (prefix.length() <= 1) {
			xpathMap.put(prefix, elt);
		}
		int length = nodes.getLength();
		short lastNodeType = Node.DOCUMENT_NODE;
		// ignore sequential texts (with wrapping), as per non-map version
		for (int i = 0; i < length; i++) {
			Node node = nodes.getItem(i);
			node = unwrapOrIgnore(node);
			if (node == null) {
				continue;
			}
			short nodeType = node.getNodeType();
			if ((nodeType == Node.TEXT_NODE && lastNodeType != Node.TEXT_NODE)
					|| nodeType == Node.ELEMENT_NODE) {
				String marker = nodeType == Node.TEXT_NODE ? DomUtils.TEXT_MARKER
						: node.getNodeName().toUpperCase();
				int c = total.containsKey(marker) ? total.get(marker) : 0;
				total.put(marker, c + 1);
			}
			lastNodeType = nodeType;
		}
		lastNodeType = Node.DOCUMENT_NODE;
		for (int i = 0; i < length; i++) {
			Node node = nodes.getItem(i);
			node = unwrapOrIgnore(node);
			if (node == null) {
				continue;
			}
			short nodeType = node.getNodeType();
			if ((nodeType == Node.TEXT_NODE && lastNodeType != Node.TEXT_NODE)
					|| nodeType == Node.ELEMENT_NODE) {
				String marker = nodeType == Node.TEXT_NODE ? DomUtils.TEXT_MARKER
						: node.getNodeName().toUpperCase();
				String post = marker;
				if (total.get(marker) != 1) {
					int c = current.containsKey(marker) ? current.get(marker)
							: 0;
					current.put(marker, ++c);
					post += "[" + c + "]";
				}
				String xp = prefix + post;
				if (debug && !xp.contains("TBODY")) {
					System.out.println(xp);
				}
				xpathMap.put(xp, node);
				if (nodeType == Node.ELEMENT_NODE) {
					generateMap0((Element) node, xp + "/", xpathMap);
					// this won't cause ambiguity
					if (post.equals("TBODY")) {
						generateMap0((Element) node, prefix, xpathMap);
					}
				} else {
					if (debug && !xp.contains("TBODY")) {
						System.out.println("\t\t" + node.getNodeValue());
					}
				}
			}
			lastNodeType = nodeType;
		}
	}

	public Node dumpNearestMatch(String xpathStr, Node container) {
		String ucXpath = xpathStr.toUpperCase();
		String[] sections = ucXpath.split("/");
		String matched = "";
		String lastMatchedPath = "";
		Node lastMatched = null;
		int count=0;
		for (String section : sections) {
			if (matched.length() != 0) {
				matched += "/";
			}
			matched += section;
			Node match = findXpathWithIndexedText(matched, container);
			if (match == null) {
				System.out.println("Prefix matched:" + lastMatchedPath
						+ "\n----------\n");
				Map<String, Node> xpathMap = new LinkedHashMap<String, Node>();
				generateMap((Element) lastMatched, "", xpathMap);
				dumpMap0(false, xpathMap);
				if(count>3){
					System.out.println("Parent map:");
					xpathMap = new LinkedHashMap<String, Node>();
					generateMap((Element) lastMatched.getParentElement(), "", xpathMap);
					dumpMap0(false, xpathMap);
				}
				return lastMatched;
			} else {
				lastMatched = match;
				lastMatchedPath=matched;
				count++;
			}
		}
		return null;
	}

	public static String toSimpleXPointer(Node n) {
		List<String> parts = new ArrayList<String>();
		while (n != null) {
			short nodeType = n.getNodeType();
			switch (nodeType) {
			case Node.DOCUMENT_NODE:
				parts.add("");
				break;
			default:
				String part = n.getNodeName();
				switch (nodeType) {
				case Node.TEXT_NODE:
					part = "TEXT()";
				}
				NodeList childNodes = n.getParentNode().getChildNodes();
				int pos = -1;
				int count = 0;
				int length = childNodes.getLength();
				for (int i = 0; i < length; i++) {
					Node item = childNodes.getItem(i);
					if (item == n) {
						pos = count + 1;
					}
					if (item.getNodeType() == nodeType) {
						if (nodeType == Node.ELEMENT_NODE) {
							if (!((Element) n).getTagName().equals(
									((Element) item).getTagName())) {
								continue;
							}
						}
						count++;
					}
				}
				parts.add(count != 1 ? part + "[" + pos + "]" : part);
				break;
			}
			n = n.getParentNode();
		}
		Collections.reverse(parts);
		return CommonUtils.join(parts, "/");
	}
	public void dumpMap(boolean regenerate) {
		dumpMap0(regenerate,xpathMap);
	}
	private void dumpMap0(boolean regenerate, Map<String, Node> xpathMap) {
		if (regenerate) {
			xpathMap = new LinkedHashMap<String, Node>();
			generateMap((Element) lastContainer, "", xpathMap);
		}
		System.out.println("---dump xpath map");
		for (String key : xpathMap.keySet()) {
			Node node = xpathMap.get(key);
			String tc = node.getNodeType() == Node.TEXT_NODE ? " - "
					+ node.getNodeValue() : "";
			System.out.println(key + tc);
		}
		System.out.println("\n---\n\n");
	}

	public static void wrap(Element wrapper, Node toWrap) {
		Element parent = (Element) toWrap.getParentNode();
		parent.insertBefore(wrapper, toWrap);
		parent.removeChild(toWrap);
		wrapper.appendChild(toWrap);
	}

	public static Element getParentElement(Element elt, String tagName) {
		while (elt != null) {
			if (elt.getTagName().equalsIgnoreCase(tagName)) {
				return elt;
			}
			elt = elt.getParentElement();
		}
		return null;
	}

	public static boolean isAncestorOf(Element ancestor, Node possibleChild) {
		Element stop = GWT.isClient() ? RootPanel.get().getElement() : null;
		while (possibleChild != null && possibleChild != stop) {
			if (possibleChild == ancestor) {
				return true;
			}
			possibleChild = possibleChild.getParentNode();
		}
		return false;
	}

	public static String stripStructuralTags(String html) {
		Element elt = Document.get().createDivElement();
		elt.setInnerHTML(html);
		boolean loopOk = true;
		while (loopOk) {
			loopOk = false;
			if (elt.getChildNodes().getLength() == 1) {
				Node child = elt.getChildNodes().getItem(0);
				if (child.getNodeType() == Node.ELEMENT_NODE) {
					Element childElt = (Element) child;
					String tag = childElt.getTagName().toLowerCase();
					if (tag.equals("div") || tag.equals("p")) {
						elt = childElt;
						loopOk = true;
					}
				}
			}
		}
		return elt.getInnerHTML();
	}

	public static Element getAncestorWithTagName(Node node, String tagName) {
		while (node != null) {
			if (node.getNodeType() == Node.ELEMENT_NODE
					&& node.getNodeName().equalsIgnoreCase(tagName)) {
				return (Element) node;
			}
			node = node.getParentNode();
		}
		return null;
	}

	public static Element getContainingBlock(Node n) {
		while (n != null) {
			if (n.getNodeType() == Node.ELEMENT_NODE
					&& isBlockHTMLElement((Element) n)) {
				return (Element) n;
			}
			n = n.getParentNode();
		}
		return null;
	}

	public static Element getNeighbouringBlock(Node n, int dir) {
		Element block = getContainingBlock(n);
		if (block == null) {
			return null;
		}
		ClientNodeIterator itr = new ClientNodeIterator(block,
				ClientNodeIterator.SHOW_ELEMENT);
		while (true) {
			Element e = (Element) (dir == 1 ? itr.nextNode() : itr
					.previousNode());
			if (e == null) {
				return null;
			}
			Element cb = getContainingBlock(e);
			if (cb != block && !isAncestorOf(block, cb)
					&& !isAncestorOf(cb, block)) {
				return cb;
			}
		}
	}

	public static class HighlightInfo {
		public String cssClassName;

		public Map<String, String> styleProperties;

		public Map<String, String> properties;

		public HighlightInfo(String cssClassName,
				Map<String, String> styleProperties,
				Map<String, String> properties) {
			this.cssClassName = cssClassName;
			this.styleProperties = styleProperties;
			this.properties = properties;
		}
	}

	public void resetCache(Node container) {
		if (useXpathMap) {
			if (lastContainer != container) {
				lastContainer = container;
				xpathMap = new LinkedHashMap<String, Node>();
				ClientNotifications notifications = ClientLayerLocator.get()
						.notifications();
				if (notifications != null) {
					notifications.metricLogStart(DOM_XPATH_MAP);
				}
				itrStack = new Stack<DomUtils.XpathMapPoint>();
				itrStack.add(new XpathMapPoint((Element) container, ""));
			}
		}
	}

	Stack<XpathMapPoint> itrStack = null;

	class XpathMapPoint {
		Element elt;

		public XpathMapPoint(Element elt, String prefix) {
			this.elt = elt;
			this.prefix = prefix;
		}

		String prefix;
	}

	public boolean iterateCache(int numberIterations) {
		for (int i = 0; i < numberIterations && !itrStack.isEmpty(); i++) {
			generateMapItr(itrStack.pop());
		}
		if (itrStack.isEmpty()) {
			ClientNotifications notifications = ClientLayerLocator.get()
					.notifications();
			if (notifications != null) {
				notifications.metricLogEnd(DOM_XPATH_MAP);
			}
		}
		return !itrStack.isEmpty();
	}

	public void generateMapItr(XpathMapPoint point) {
		Map<String, Integer> total = new HashMap<String, Integer>();
		Map<String, Integer> current = new HashMap<String, Integer>();
		Element elt = point.elt;
		String prefix = point.prefix;
		NodeList<Node> nodes = elt.getChildNodes();
		if (prefix.length() <= 1) {
			xpathMap.put(prefix, elt);
		}
		int length = nodes.getLength();
		for (int i = 0; i < length; i++) {
			Node node = nodes.getItem(i);
			short nodeType = node.getNodeType();
			if (nodeType == Node.TEXT_NODE || nodeType == Node.ELEMENT_NODE) {
				String marker = nodeType == Node.TEXT_NODE ? DomUtils.TEXT_MARKER
						: node.getNodeName().toUpperCase();
				int c = total.containsKey(marker) ? total.get(marker) : 0;
				total.put(marker, c + 1);
			}
		}
		for (int i = 0; i < length; i++) {
			Node node = nodes.getItem(i);
			short nodeType = node.getNodeType();
			if (nodeType == Node.TEXT_NODE || nodeType == Node.ELEMENT_NODE) {
				String marker = nodeType == Node.TEXT_NODE ? DomUtils.TEXT_MARKER
						: node.getNodeName().toUpperCase();
				String post = marker;
				if (total.get(marker) != 1) {
					int c = current.containsKey(marker) ? current.get(marker)
							: 0;
					current.put(marker, ++c);
					post += "[" + c + "]";
				}
				String xp = prefix + post;
				if (debug && !xp.contains("TBODY")) {
					System.out.println(xp);
				}
				xpathMap.put(xp, node);
				if (nodeType == Node.ELEMENT_NODE) {
					itrStack.push(new XpathMapPoint((Element) node, xp + "/"));
					// this won't cause ambiguity
					if (post.equals("TBODY")) {
						itrStack.push(new XpathMapPoint((Element) node, prefix));
					}
				} else {
					if (debug && !xp.contains("TBODY")) {
						System.out.println("\t\t" + node.getNodeValue());
					}
				}
			}
		}
	}

	public void setNodeProvider(NodeFromXpathProvider nodeProvider) {
		this.nodeProvider = nodeProvider;
	}

	public NodeFromXpathProvider getNodeProvider() {
		return nodeProvider;
	}

	public static String toText(String html) {
		Element elt = Document.get().createElement("DIV");
		elt.setInnerHTML(html);
		return DOM.getInnerText((com.google.gwt.user.client.Element) elt);
	}

	public static Element getMinimalParentWithOffsetHeight(Text text) {
		Element parent = text.getParentElement();
		while (parent != null) {
			if (parent.getOffsetHeight() != 0) {
				return parent;
			}
			parent = parent.getParentElement();
		}
		return null;
	}

	public static boolean containsBlocks(Element elt) {
		elt.getChildNodes();
		for (int i = 0; i < elt.getChildCount(); i++) {
			Node child = elt.getChild(i);
			if (child.getNodeType() == Node.ELEMENT_NODE
					&& isBlockHTMLElement((Element) child)) {
				return true;
			}
		}
		return false;
	}

	public static List<Element> nodeListToElementList(NodeList list) {
		List<Element> result = new ArrayList<Element>();
		int length = list.getLength();
		for (int i = 0; i < length; i++) {
			Node node = list.getItem(i);
			if (node.getNodeType() == Node.ELEMENT_NODE) {
				result.add((Element) node);
			}
		}
		return result;
	}

	public static List<Element> getChildElements(Element elt) {
		return nodeListToElementList(elt.getChildNodes());
	}

	public void invalidateUnwrapOrIgnoreCache() {
		unwrapOrIgnoreCache = new IdentityHashMap<Node, Node>(10000);
	}

	public static interface DomUtilsFactory {
		public DomUtils create();
	}

	public static class DomUtilsFactoryDefault implements DomUtilsFactory {
		public DomUtils create() {
			return new DomUtils();
		}
	}

	public void dumpContainerNoMap() {
		Node lc = lastContainer;
		System.out.println("---dump xpath map (no map)");
		useXpathMap = false;
		for (String key : xpathMap.keySet()) {
			Node node = findXpathWithIndexedText(key, lastContainer);
			if (node == null) {
				System.out.println("***MISSING***" + key);
			} else {
				String tc = node.getNodeType() == Node.TEXT_NODE ? " - "
						+ node.getNodeValue() : "";
				System.out.println(key + tc);
			}
		}
		System.out.println("\n---\n\n");
		useXpathMap = true;
	}
}
